package br.usp.tuf.utils;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import br.usp.tuf.annotations.ConstructorJNeticTest;
import br.usp.tuf.entities.ConstrutorVisao;
import br.usp.tuf.entities.MetodoVisao;
import br.usp.tuf.entities.NodeItem;

import com.google.java.contract.Ensures;
import com.google.java.contract.Invariant;
import com.google.java.contract.Requires;

/**
 * Classe com métodos úteis para utilização dos conceitos de Reflection.
 * 
 * @author Venilton Falvo Jr
 */
public final class ReflectionUtils {

	private static final String PREFIXO_METODOS_RUNTIME_COFOJA = "com$google$java$contract$";

	/**
	 * Construtor privado. Garante que esta classe sirva apenas para
	 * disponibilização de métodos estáticos.
	 */
	private ReflectionUtils() {
		super();
	}

	/**
	 * Método que acessa via Reflection todos os métodos da classe passada como
	 * parâmetro. As informações dos métodos são armazenadas na classe
	 * {@link MetodoVisao}, que posteriormente é adicionada em uma {@link List}
	 * para retorno.
	 * 
	 * @param classe
	 *            Classe de onde os métodos serão carregados.
	 * @return
	 */
	public static List<MetodoVisao> carregarMetodos(Class<?> classe) {
		final List<MetodoVisao> metodosVisao = new ArrayList<MetodoVisao>();
		final Method[] metodos = classe.getDeclaredMethods();
		for (final Method metodo : metodos) {
			if (!metodo.getName().startsWith(PREFIXO_METODOS_RUNTIME_COFOJA)) {
				final MetodoVisao metodoVisao = new MetodoVisao();
				// Seta o nome:
				metodoVisao.setNome(metodo.getName());
				// Seta o modificador:
				metodoVisao.setModificador(Modifier.toString(metodo.getModifiers()));
				// Caso existam parâmetros, os mesmos são armazenados em uma lista:
				final Class<?>[] parametros =  metodo.getParameterTypes();
				for (final Class<?> parametro : parametros) {
					metodoVisao.getParametros().add(parametro);
				}
				// Seta o tipo retorno.
				metodoVisao.setRetorno(metodo.getReturnType());
				
				// Set as Annotations do Cofoja
				final Ensures posCondicoes = metodo.getAnnotation(Ensures.class);
				final Requires preCondicoes = metodo.getAnnotation(Requires.class);
				final Invariant invariantes = metodo.getAnnotation(Invariant.class);
				if (posCondicoes != null) {
					metodoVisao.getPosCondicoes().addAll(Arrays.asList(posCondicoes.value()));
				} else if (preCondicoes != null) {
					metodoVisao.getPreCondicoes().addAll(Arrays.asList(preCondicoes.value()));
				} else if (invariantes != null) {
					metodoVisao.getInvariantes().addAll(Arrays.asList(invariantes.value()));
				}
				
				metodosVisao.add(metodoVisao);
			}
		}
		return metodosVisao;
	}
	
	public static Constructor<?> recuperarConstrutorJNeticTest(Class<?> classe) {
		Constructor<?> retorno = null;
		final Constructor<?>[] construtores = classe.getDeclaredConstructors();
		for (final Constructor<?> constructor : construtores) {
			if (constructor.getAnnotation(ConstructorJNeticTest.class) != null) {
				retorno = constructor;
				break;
			}
		}
		return retorno;
	}
	
	public static Constructor<?> recuperarConstrutorPadrao(Class<?> classe) {
		Constructor<?> retorno = null;
		final Constructor<?>[] construtores = classe.getDeclaredConstructors();
		for (final Constructor<?> constructor : construtores) {
			if (constructor.getParameterTypes().length == 0) {
				retorno = constructor;
				break;
			}
		}
		return retorno;
	}
	
	public static Object instanciarNo(NodeItem no) throws Exception {
		Object retorno = null;
		if (no.getValor() == null) {
			if (GeradorAleatorio.isPrimitivo(no.getNome())) {
				retorno = GeradorAleatorio.get(no.getNome());
			} else {
				final Constructor<?> construtor = recuperarConstrutorJNeticTest(Class.forName(no.getNome()));
				if (construtor != null) {
					final int qtdParametros = construtor.getParameterTypes().length;
					if (qtdParametros > 0) {
						final Object[] objetos = new Object[qtdParametros];
						int contador = 0;
						for (NodeItem noConstrutor : no.getFilhos()) {
							objetos[contador] = instanciarNo(noConstrutor);
							contador++;
						}
						if (contador == qtdParametros) {
							retorno = construtor.newInstance(objetos);
						} 
					} else {
						retorno = construtor.newInstance();
					}			
				}
			}
		} else {
			retorno = no.getValor();
		}
		return retorno;
	}
	
	public static ConstrutorVisao carregarConstrutores(Class<?> classe) {	
		ConstrutorVisao construtorVisao = new ConstrutorVisao();
		Constructor<?>[] construtores = classe.getConstructors();
		construtorVisao.setConstructor(Arrays.asList(construtores));
		return construtorVisao;
	
	}
	
	public static  Class<?>[]  carregarParametrosDoConstrutor(Constructor<?> constructor) {
		 Class<?>[]  parametros = constructor.getParameterTypes();
		 return parametros;
	}
	
	/**
	 * Método que recupera um {@link Field} que representa um atributo declarado
	 * no objeto passado como parâmetro.
	 * 
	 * @param objetoPai
	 *            {@link Object} que carrega o atributo desejado.
	 * @param nomeDoAtributo
	 *            Nome do atributo que será recuperado.
	 * @return
	 * @throws NoSuchFieldException
	 * @throws SecurityException
	 */
	public static Field get(Object objetoPai, String nomeDoAtributo) throws Exception {
		final Field atributo = objetoPai.getClass().getDeclaredField(nomeDoAtributo);
		atributo.setAccessible(true);
		return atributo;
	}

	/**
	 * Método que recupera um atributo de uma classe pai e posteriormente
	 * modifica seu valor através de uma instância.
	 * 
	 * @param objetoPai
	 *            {@link Object} que carrega o atributo desejado.
	 * @param nomeDoAtributo
	 *            Nome do atributo que será modificado.
	 * @param valor
	 *            Valor que será setado.
	 * 
	 * @throws NoSuchFieldException
	 * @throws IllegalAccessException
	 * @throws SecurityException
	 * @throws IllegalArgumentException
	 */
	public static void set(Object objetoPai, String nomeDoAtributo, Object valor) throws Exception {
		final Field atributo = get(objetoPai, nomeDoAtributo);
		atributo.setAccessible(true);
		atributo.set(objetoPai, valor);
	}

	/**
	 * Método que executa por reflexão um método de uma {@link Object} passado
	 * como parâmetro.
	 * 
	 * @param objetoPai
	 *            {@link Object} que carrega o método desejado.
	 * @param nomeDoAtributo
	 *            Nome do método que será executado.
	 * @param parametros
	 *            Conjunto de parâmetros do método que será executado.
	 * @return
	 * @throws InvocationTargetException
	 * @throws IllegalAccessException
	 * @throws IllegalArgumentException
	 * @throws ReflectiveOperationException
	 */
	public static Object executarMetodo(Object objetoPai, String nomeDoMetodo, Object... parametros) throws Exception {
		for (Method metodo : objetoPai.getClass().getDeclaredMethods()) {
			if (nomeDoMetodo.equals(metodo.getName())) {
				metodo.setAccessible(true);
				return metodo.invoke(objetoPai, parametros);
			}
		}
		return null;
	}
}
